package edu.northwestern.cbits.purple_robot_manager.models; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.net.MalformedURLException; import java.net.URL; import java.util.HashMap; import java.util.Map; import java.util.UUID; import org.apache.commons.io.FileUtils; import org.json.JSONException; import org.json.JSONObject; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.content.res.AssetManager; import android.net.Uri; import android.os.Bundle; import android.preference.CheckBoxPreference; import android.preference.PreferenceManager; import android.preference.PreferenceScreen; import android.support.v4.content.LocalBroadcastManager; import edu.northwestern.cbits.purple_robot_manager.EncryptionManager; import edu.northwestern.cbits.purple_robot_manager.R; import edu.northwestern.cbits.purple_robot_manager.activities.settings.SettingsActivity; import edu.northwestern.cbits.purple_robot_manager.logging.LogManager; import edu.northwestern.cbits.purple_robot_manager.probes.Probe; /** * Provides the structure for Models. Models take input from probes, features, * and other models and generate predictions from those inputs. Purple Robot * supports both linear (e.g. regressions) and categorical models (e.g. decision * trees). * * In Purple Robot, models are defined by providing a URL to the model, which * contains a JSON representation of the model that can be deserialized into a * functioning class instance that implements the given model. Purple Robot * caches model definitions, allowing the system to access and construct model * instances even when the device is unable to access the original definition's * URL due to a lack of reliable network connectivity. * * Please see the various Model subclasses for specific implementation details. */ /** * @author Administrator * */ public abstract class Model { public static final boolean DEFAULT_ENABLED = true; private static long _lastEnabledCheck = 0; private static boolean _lastEnabled = false; protected HashMap<String, String> _featureMap = new HashMap<>(); // Cached values used to determine when the model state has changed. private Object _latestPrediction = null; private double _latestAccuracy = 0.0; /** * Provides a lookup key used to generate and configure a given model. * * @return Value used internally to manipulate model settings via the * SharedPreferences mechanism. */ public abstract String getPreferenceKey(); /** * Provides a human-readable name for the model used throughout the user * interface. * * @param context * Android Context object used to resolve values such as strings. * * @return Human-readable model name. */ public abstract String title(Context context); /** * Provides a human-readable description of the model used throughout the * user interface. * * @param context * Android Context object used to resolve values such as strings. * * @return Human-readable model description. */ public abstract String summary(Context context); /** * Reads the model definition from an online URL and attempts to construct * an appropriate dynamic model instance using the model subclasses. For * example, a decision tree may be constructed from the TreeModel class, * while a linear equation may be expanded using the {@link RegressionModel} * class. * * @param context * Android Context object used to lookup internal storage * destinations. * @param jsonUrl * HTTP URL pointing to a model definition. * * @return Model instance implementing model specified, null if no * appropriate {@link Model} class can be determined. */ public static Model modelForUrl(Context context, String jsonUrl) { String hash = EncryptionManager.getInstance().createHash(context, jsonUrl, "MD5"); SharedPreferences prefs = Probe.getPreferences(context); // Determine where to cache the contents of jsonUrl... File internalStorage = context.getFilesDir(); if (SettingsActivity.useExternalStorage(context)); internalStorage = context.getExternalFilesDir(null); if (internalStorage != null && !internalStorage.exists()) internalStorage.mkdirs(); File modelsFolder = new File(internalStorage, "persisted_models"); if (modelsFolder != null && !modelsFolder.exists()) modelsFolder.mkdirs(); String contents = null; File cachedModel = new File(modelsFolder, hash); // Load any cached model definitions... try { contents = FileUtils.readFileToString(cachedModel); } catch (IOException e) { } // Fetch the contents of the URL & replace the cached version if // successful retrieving the definition online... try { BufferedReader in = null; if (jsonUrl.startsWith("file:///android_asset/")) { AssetManager assets = context.getAssets(); in = new BufferedReader(new InputStreamReader( assets.open(jsonUrl.replace("file:///android_asset/", "")))); } else { URL u = new URL(jsonUrl); in = new BufferedReader(new InputStreamReader(u.openStream())); } StringBuilder sb = new StringBuilder(); String inputLine = null; while ((inputLine = in.readLine()) != null) sb.append(inputLine); in.close(); contents = sb.toString(); } catch (IOException e) { LogManager.getInstance(context).logException(e); } // Determine which subclass to use to instantiate an instance. Return // the new instance if successful... if (contents != null) { try { JSONObject json = new JSONObject(contents); String type = json.getString("model_type"); if (RegressionModel.TYPE.equals(type)) return new RegressionModel(context, Uri.parse(jsonUrl)); else if (WekaTreeModel.TYPE.equals(type)) return new WekaTreeModel(context, Uri.parse(jsonUrl)); else if (MatlabForestModel.TYPE.equals(type)) return new MatlabForestModel(context, Uri.parse(jsonUrl)); else if (MatlabTreeModel.TYPE.equals(type)) return new MatlabTreeModel(context, Uri.parse(jsonUrl)); else if (MatlabForestModel.TYPE.equals(type)) return new MatlabForestModel(context, Uri.parse(jsonUrl)); else if (NoiseModel.TYPE.equals(type)) return new NoiseModel(context, Uri.parse(jsonUrl)); } catch (JSONException e) { LogManager.getInstance(context).logException(e); } } // ... and return null if something went wrong. return null; } /** * @return Uri referencing the model. May be the original URL of the model, * but may also be an alternative Uri as the situation permits. * Model implementation returns null by default - subclasses * implement alternative behaviors. */ public Uri uri() { return null; } /** * Enables the model. This method is used by other parts of the system such * as the scripting framework and user-facing settings. * * @param context * Android Context object used to access the SharedPreferences * instances. */ public void enable(Context context) { String key = this.getPreferenceKey(); SharedPreferences prefs = Probe.getPreferences(context); Editor e = prefs.edit(); e.putBoolean("config_model_" + key + "_enabled", true); e.commit(); } /** * Disables the model. This method is used by other parts of the system such * as the scripting framework and user-facing settings. * * @param context * Android Context object used to access the SharedPreferences * instances. */ public void disable(Context context) { String key = this.getPreferenceKey(); SharedPreferences prefs = Probe.getPreferences(context); Editor e = prefs.edit(); e.putBoolean("config_model_" + key + "_enabled", false); e.commit(); } /** * Reports whether a model instance is enabled. Note that models may be * enabled and disabled on an individual basis or all may be disabled from * the {@link ModelManager} singleton. * * @param context * Android Context object used to access the SharedPreferences * instances * * @return Status of the model. */ public boolean enabled(Context context) { if (ModelManager.getInstance(context).enabled(context)) { String key = this.getPreferenceKey(); SharedPreferences prefs = Probe.getPreferences(context); return prefs.getBoolean("config_model_" + key + "_enabled", Model.DEFAULT_ENABLED); } return false; } /** * Constructs a preference screen dynamically for an infividual model * instance for used in the user-facing app settings. * * @param context * Parent settings activity. * * @return Screen containing all the relevant options for a model instance. */ @SuppressWarnings("deprecation") public PreferenceScreen preferenceScreen(Context context, PreferenceManager manager) { PreferenceScreen screen = manager.createPreferenceScreen(context); screen.setTitle(this.title(context)); screen.setSummary(this.summary(context)); String key = this.getPreferenceKey(); CheckBoxPreference enabled = new CheckBoxPreference(context); enabled.setTitle(R.string.title_enable_model); enabled.setKey("config_model_" + key + "_enabled"); enabled.setDefaultValue(Model.DEFAULT_ENABLED); screen.addPreference(enabled); return screen; } /** * Called periodically by the rest of the system to determine if the model * is enabled. TODO: "isEnabled" is a convention adopted from the Probe * classes, and needs to be refactored, along with these instances as well. * ("nudge" might be a better name. * * @param context * Android Context object used to access the SharedPreferences * instances * * @return Status of the model. */ public boolean isEnabled(Context context) { long now = System.currentTimeMillis(); SharedPreferences prefs = Probe.getPreferences(context); if (now - Model._lastEnabledCheck > 10000) { Model._lastEnabledCheck = now; Model._lastEnabled = prefs.getBoolean("config_models_enabled", Model.DEFAULT_ENABLED); } if (Model._lastEnabled) { String key = this.getPreferenceKey(); return prefs.getBoolean("config_model_" + key + "_enabled", true); } return Model._lastEnabled; } /** * Transmits a continuous prediction (real number) generated by the model to * the rest of the data processing pipeline. * * @param context * @param prediction * Value of the prediction. * @param accuracy * Estimated accuracy of the prediction. */ protected void transmitPrediction(Context context, double prediction, double accuracy) { Bundle bundle = new Bundle(); bundle.putString("PROBE", this.title(context)); bundle.putDouble("TIMESTAMP", ((double) System.currentTimeMillis()) / 1000); bundle.putDouble("PREDICTION", prediction); bundle.putBoolean("FROM_MODEL", true); this.transmitData(context, bundle); this._latestPrediction = prediction; this._latestAccuracy = accuracy; } /** * Provides the latest prediction made by the model. * * @param context * @return Map containing the prediction and any relevant metadata. */ public Map<String, Object> latestPrediction(Context context) { HashMap<String, Object> prediction = new HashMap<>(); prediction.put("prediction", this._latestPrediction); prediction.put("accuracy", this._latestAccuracy); prediction.put("type", this.modelType()); prediction.put("title", this.title(context)); prediction.put("url", this.name(context).replace("\\/", "/")); return prediction; } /** * Transmits a classification (string label) generated by the model to the * rest of the data processing pipeline. * * @param context * @param prediction * Value of the prediction. * @param accuracy * Estimated accuracy of the prediction. * @param map */ protected void transmitPrediction(Context context, String prediction, double accuracy, Map<String, Object> map) { Bundle bundle = new Bundle(); bundle.putString("PROBE", this.title(context)); bundle.putDouble("TIMESTAMP", ((double) System.currentTimeMillis()) / 1000); bundle.putString("PREDICTION", prediction); bundle.putBoolean("FROM_MODEL", true); bundle.putDouble("ACCURACY", accuracy); if (map != null) { for (String key : map.keySet()) { Object value = map.get(key); if (value instanceof Double) bundle.putDouble(key, (Double) value); else if (value instanceof Integer) bundle.putInt(key, (Integer) value); else bundle.putString(key, value.toString()); } } this.transmitData(context, bundle); this._latestPrediction = prediction; this._latestAccuracy = accuracy; } protected void transmitPrediction(Context context, String prediction, double accuracy) { this.transmitPrediction(context, prediction, accuracy, null); } /** * Utility function used by the transmitPrediction methods to broadcast a * model's predictions to the rest of the data processing pipeline. * * @param context * @param data * Bundle containing the prediction and any relevant metadata. */ protected void transmitData(Context context, Bundle data) { if (context != null) { UUID uuid = UUID.randomUUID(); data.putString("GUID", uuid.toString()); data.putString("MODEL_NAME", this.title(context)); LocalBroadcastManager localManager = LocalBroadcastManager.getInstance(context); Intent intent = new Intent(edu.northwestern.cbits.purple_robot_manager.probes.Probe.PROBE_READING); intent.putExtras(data); localManager.sendBroadcast(intent); } } /** * Called when a model prediction is requested. Note that this only requests * a prediction - models are not obligated to return a prediction * immediately, but instead will generate a prediction asynchronously and * share the prediction using the transmitPrediction methods. * * @param context * @param snapshot * A representation of the state of the world to be used to * generate a prediction. */ public abstract void predict(Context context, Map<String, Object> snapshot); /** * Returns the name of the model used internally (not human-readable). In * most cases, this will be the URL of the model's definition file. * * @param context * * @return Unique string identifying a model instance. */ public abstract String name(Context context); /** * @return Identifier for the type of the model. */ public abstract String modelType(); public String mappedFeatureName(String key) { return this._featureMap.get(key); } }